#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import sys
import time
import manpa
import shutil
import jinja2
import socket
import subprocess
import mirrors.plugin
from selenium.webdriver.common.by import By


class Main:

    def __init__(self):
        self.cfg = mirrors.plugin.params["config"]
        self.dataDir = mirrors.plugin.params["storage-file"]["data-directory"]
        self.i2pDir = os.path.join(self.dataDir, ".i2p")                        # FIXME

        self.mp = manpa.Manpa()
        self.p = InfoPrinter()

    def run(self):
        selfDir = os.path.dirname(os.path.realpath(__file__))
        wwwDir = os.path.join(selfDir, "www")

        url = "https://fairy-stockfish.github.io/nnue"
        networkCompatibleMap = {
            "5check": "3check",
            "cambodian": "makruk",
            "placement": "chess",
        }

        # get information
        networkList = []
        self.p.print("Get information from %s:" % (url))
        self.p.incIndent()
        try:
            with self.mp.open_selenium_client() as driver:
                driver.get_and_wait(url)
                for trTag in driver.find_elements(by=By.XPATH, value=".//section//table/tbody/tr"):
                    tdList = trTag.find_elements(by=By.XPATH, value="./td")
                    item = {}
                    if tdList[0].text not in networkCompatibleMap.keys():
                        aTag = tdList[1].find_elements(by=By.XPATH, value="./a")[0]
                        item["name"] = tdList[0].text
                        item["filename"] = aTag.text
                        item["link"] = aTag.get_attribute("href")
                        item["elo"] = tdList[2].text
                        item["date"] = tdList[3].text.strip()
                    else:
                        item["name"] = tdList[0].text
                        item["filename"] = ""
                        item["link"] = ""
                        item["elo"] = ""
                        item["date"] = ""
                    networkList.append(item)

            for item in networkList:
                if item["name"] in networkCompatibleMap.keys():
                    item2 = [x for x in networkList if x["name"] == networkCompatibleMap[item["name"]]]
                    if len(item2) == 0:
                        continue
                    item2 = item2[0]
                    item["filename"] = item2["filename"]
                    item["link"] = item2["link"]
                    item["elo"] = item2["elo"]
                    item["date"] = item2["date"]

            for item in networkList:
                self.p.print("network: variant: %s, elo-vs-classical: %s, date: %s, filename: %s, link: %s" % (item["name"], item["elo"], item["date"], item["filename"], item["link"]))
        finally:
            self.p.decIndent()

        # download nnue networks
        self.p.print("Download NNUE networks:")
        self.p.incIndent()
        try:
            os.makedirs(self.i2pDir, exist_ok=True)

            i2ps = TmpI2pServer(self.i2pDir)

            print("Staring I2P server...")
            i2ps.start()
            print("Staring I2P server complete, port: %d." % (i2ps.get_proxy_port()))
            try:
                fullDir = os.path.join(self.dataDir, "data")
                os.makedirs(fullDir, exist_ok=True)
                self._downloadFilesToDirThenClearDir(i2ps, set([(x["filename"], x["link"]) for x in networkList]), fullDir)
            finally:
                i2ps.stop()
        finally:
            self.p.decIndent()

        # generate www directory
        self.p.print("Generate www directory:")
        if True:
            Util.forceDelete(os.path.join(self.dataDir, "css"))
            shutil.copytree(os.path.join(wwwDir, "css"), os.path.join(self.dataDir, "css"))

            # generate web pages
            tm = jinja2.Environment(loader=jinja2.FileSystemLoader(wwwDir)).get_template("index.html.jinja2")
            with open(os.path.join(self.dataDir, "index.html"), "w") as f:
                f.write(tm.render({
                    "network_list": networkList,
                }))

    def _downloadFilesToDirThenClearDir(self, i2ps, itemSet, dstDir):
        envDict = {
            'http_proxy': 'http://localhost:%d' % (i2ps.get_proxy_port()),
            'https_proxy': 'http://localhost:%d' % (i2ps.get_proxy_port()),
        }
        envDict.update(os.environ)

        fnList = os.listdir(dstDir)
        for newFn, url in itemSet:
            localFile = os.path.join(dstDir, newFn)
            try:
                fnList.remove(newFn)
                self.p.print("file already downloaded: %s" % (localFile))
                continue
            except ValueError:
                pass

            tmpFullFn = ".downloading"
            while True:
                try:
                    subprocess.check_call(["wget", "--quiet", "--no-check-certificate", "-O", tmpFullFn, url], env=envDict)      # always re-dowloand
                    break
                except subprocess.CalledProcessError as e:
                    if e.returncode == 8:       # not found
                        raise
                    self.p.print("download failed and try again: %s" % str(e))
                    time.sleep(60)
            os.rename(tmpFullFn, localFile)
            self.p.print("file downloaded: %s" % (localFile))

        for fn in fnList:
            Util.forceDelete(os.path.join(dstDir, fn))


class InfoPrinter:

    class _InfoPrinterIndenter:

        def __init__(self, parent, message):
            self._parent = parent
            self._parent.print(message)
            self._parent.incIndent()

        def __enter__(self):
            return self

        def __exit__(self, type, value, traceback):
            self._parent.decIndent()

    def __init__(self):
        self.indent = 0

    def incIndent(self):
        self.indent = self.indent + 1

    def decIndent(self):
        assert self.indent > 0
        self.indent = self.indent - 1

    def print(self, s):
        line = ""
        line += "\t" * self.indent
        line += s
        print(line)

    def printAndIndent(self, s):
        return self._InfoPrinterIndenter(self, s)


# FIXME: we should not use our own i2p server, and, manual filling self.i2pDir for bootstraping is needd
class TmpI2pServer:

    def __init__(self, i2pDir):
        self._tmppath = i2pDir
        self._proc = None

    def start(self):
        assert self._proc is None

        self._cfgFile = os.path.join(self._tmppath, "i2p.conf")
        self._pidFile = os.path.join(self._tmppath, "i2p.pid")
        self._logFile = os.path.join(self._tmppath, "i2p.log")
        self._tunnelsFile = os.path.join(self._tmppath, "tunnels.cfg")
        self._tunnelsDir = os.path.join(self._tmppath, "tunnels.d")

        self._proxyPort = Util.getFreeSocketPort("tcp")

        buf = ""
        buf += "tunconf = %s\n" % (self._tunnelsFile)
        buf += "tunnelsdir = %s\n" % (self._tunnelsDir)
        buf += "pidfile = %s\n" % (self._pidFile)
        buf += "logfile = %s\n" % (self._logFile)
        buf += "\n"
        buf += "ipv4 = true\n"
        buf += "ipv6 = true\n"
        buf += "ssu = true\n"
        buf += "\n"
        buf += "[ntcp2]\n"
        buf += "\n"
        buf += "[ssu2]\n"
        buf += "\n"
        buf += "[http]\n"
        buf += "enabled = false\n"
        buf += "\n"
        buf += "[httpproxy]\n"
        buf += "address = 127.0.0.1\n"
        buf += "port = %d\n" % (self._proxyPort)
        buf += "outproxy = http://outproxy.acetone.i2p:3128\n"
        buf += "outbound.length = 1\n"
        buf += "inbound.length = 1\n"
        buf += "\n"
        buf += "[sam]\n"
        buf += "enabled = true\n"
        buf += "\n"
        buf += "[bob]\n"
        buf += "\n"
        buf += "[i2cp]\n"
        buf += "\n"
        buf += "[i2pcontrol]\n"
        buf += "\n"
        buf += "[precomputation]\n"
        buf += "\n"
        buf += "[upnp]\n"
        buf += "enabled = true\n"
        buf += "name = I2Pd\n"
        buf += "\n"
        buf += "[meshnets]\n"
        buf += "\n"
        buf += "[reseed]\n"
        buf += "verify = true\n"
        buf += "\n"
        buf += "[addressbook]\n"
        buf += "\n"
        buf += "[limits]\n"
        buf += "\n"
        buf += "[trust]\n"
        buf += "\n"
        buf += "[exploratory]\n"
        buf += "\n"
        buf += "[persist]\n"
        buf += "\n"
        buf += "[cpuext]\n"
        buf += "\n"
        with open(self._cfgFile, "w") as f:
            f.write(buf)

        self._proc = subprocess.Popen(["i2pd", "--service", "--conf", self._cfgFile, "--datadir", self._tmppath],
                                      stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

        # FIXME: wait for the tunnels prepared
        time.sleep(10)

    def get_proxy_port(self):
        assert self._proc is not None
        return self._proxyPort

    def stop(self):
        assert self._proc is not None
        self._proc.terminate()
        self._proc.wait()
        self._proc = None


class Util:

    @staticmethod
    def forceDelete(path):
        if os.path.islink(path):
            os.remove(path)
        elif os.path.isfile(path):
            os.remove(path)
        elif os.path.isdir(path):
            shutil.rmtree(path)
        elif os.path.lexists(path):
            os.remove(path)             # other type of file, such as device node
        else:
            pass                        # path does not exist, do nothing

    @staticmethod
    def forceMove(srcPath, dstPath):
        Util.forceDelete(dstPath)                # FIXME: needed?
        shutil.move(srcPath, dstPath)

    @staticmethod
    def getFreeSocketPort(portType):
        if portType == "tcp":
            stlist = [socket.SOCK_STREAM]
        elif portType == "udp":
            stlist = [socket.SOCK_DGRAM]
        elif portType == "tcp+udp":
            stlist = [socket.SOCK_STREAM, socket.SOCK_DGRAM]
        else:
            assert False

        for port in range(10000, 65536):
            bFound = True
            for sType in stlist:
                s = socket.socket(socket.AF_INET, sType)
                try:
                    s.bind((('', port)))
                except socket.error:
                    bFound = False
                finally:
                    s.close()
            if bFound:
                return port

        raise Exception("no valid port")


###############################################################################

if __name__ == "__main__":
    with mirrors.plugin.ApiClient() as sock:
        try:
            Main().run()
            sock.progress_changed(100)
        except Exception:
            sock.error_occured(sys.exc_info())
            raise
